package bulletinimpl

import (
	"encoding/xml"
	"fmt"
	"strconv"
	"strings"

	"github.com/astaxie/beego/logs"

	"cvevulner/cve-ddd/domain"
	"cvevulner/cve-ddd/domain/dp"
	"cvevulner/taskhandler"
)

const (
	src     = "src"
	x8664   = "x86_64"
	aarch64 = "aarch64"
	noarch  = "noarch"

	lang      = "en"
	openeuler = "openEuler"

	productName = "Product Name"
	packageArch = "Package Arch"
)

var archs = map[string]bool{
	aarch64: true,
	noarch:  true,
	x8664:   true,
	src:     true,
}

func NewBulletinImpl() bulletinImpl {
	cfg := Config{
		XmlnsCvrf:                 "http://www.icasi.org/CVRF/schema/cvrf/1.1",
		XmlnsProd:                 "http://www.icasi.org/CVRF/schema/prod/1.1",
		DocumentType:              "Security Advisory",
		ContactDetails:            "openeuler-release@openeuler.org",
		IssuingAuthority:          "openEuler release SIG",
		SecurityCveUrlPrefix:      "https://www.openeuler.org/en/security/cve/detail/?",
		SecurityBulletinUrlPrefix: "https://www.openeuler.org/zh/security/security-bulletins/detail/?id=",
	}

	return bulletinImpl{
		cfg: cfg,
	}
}

type bulletinImpl struct {
	cfg Config
}

func (impl bulletinImpl) GenerateColdPatch(sb *domain.SecurityBulletin) ([]byte, error) {
	data := Cvrf{
		Xmlns:              impl.cfg.XmlnsCvrf,
		XmlnsCvrf:          impl.cfg.XmlnsCvrf,
		DocumentTitle:      impl.documentTitle(sb),
		DocumentType:       impl.cfg.DocumentType,
		DocumentPublisher:  impl.documentPublisher(sb),
		DocumentTracking:   impl.documentTracking(sb),
		DocumentNotes:      impl.documentNotes(sb),
		DocumentReferences: impl.documentReferences(sb),
		ProductTree:        impl.ProductTree(sb),
		Vulnerability:      impl.vulnerability(sb),
	}

	bData, err := xml.MarshalIndent(data, "", "\t")
	if err != nil {
		return nil, err
	}

	return append([]byte(xml.Header), bData...), nil
}

func (impl bulletinImpl) GenerateHotPatch(sb *domain.SecurityBulletin) ([]byte, error) {
	data := Cvrf{
		Xmlns:              impl.cfg.XmlnsCvrf,
		XmlnsCvrf:          impl.cfg.XmlnsCvrf,
		DocumentTitle:      impl.documentTitle(sb),
		DocumentType:       impl.cfg.DocumentType,
		DocumentPublisher:  impl.documentPublisher(sb),
		DocumentTracking:   impl.documentTracking(sb),
		DocumentNotes:      impl.documentNotes(sb),
		DocumentReferences: impl.documentReferences(sb),
		HotPatchTree:       impl.HotPatchTree(sb),
		Vulnerability:      impl.vulnerability(sb),
	}

	bData, err := xml.MarshalIndent(data, "", "\t")
	if err != nil {
		return nil, err
	}

	return append([]byte(xml.Header), bData...), nil
}

func (impl bulletinImpl) joinVersion(sb *domain.SecurityBulletin) string {
	return strings.Trim(strings.Join(sb.AffectedVersion, ","), ",")
}

func (impl bulletinImpl) documentTitle(sb *domain.SecurityBulletin) DocumentTitle {
	title := fmt.Sprintf("An update for %s is now available for %s", sb.Component, impl.joinVersion(sb))

	return DocumentTitle{
		XmlLang:       lang,
		DocumentTitle: title,
	}
}

func (impl bulletinImpl) documentPublisher(sb *domain.SecurityBulletin) DocumentPublisher {
	details := impl.cfg.ContactDetails
	authority := impl.cfg.IssuingAuthority

	if sb.IsColdPatch() {
		details = "openeuler-security@openeuler.org"
		authority = "openEuler security committee"
	}

	return DocumentPublisher{
		Type:             "Vendor",
		ContactDetails:   details,
		IssuingAuthority: authority,
	}
}

func (impl bulletinImpl) documentTracking(sb *domain.SecurityBulletin) DocumentTracking {
	var engine string
	if sb.IsColdPatch() {
		engine = "openEuler SA Tool V1.0"
	} else {
		engine = "openEuler HotPatchSA Tool V1.0"
	}

	return DocumentTracking{
		Identification: Identification{
			Id: sb.Identification,
		},
		Status:  "Final",
		Version: "1.0",
		RevisionHistory: RevisionHistory{
			Revision: []Revision{{
				Number:      "1.0",
				Date:        sb.Date,
				Description: "Initial",
			}},
		},
		InitialReleaseDate: sb.Date,
		CurrentReleaseDate: sb.Date,
		Generator: Generator{
			Engine: engine,
			Date:   sb.Date,
		},
	}
}

func (impl bulletinImpl) documentNotes(sb *domain.SecurityBulletin) DocumentNotes {
	var description, topic string
	var highestLevelIndex int
	var maxScore float64

	for _, cve := range sb.Cves {
		if description == "" {
			description = cve.ComponentDesc + "Security Fix(es):"
		}
		description += fmt.Sprintf("\n\n%s(%s)", cve.CveBrief, cve.CveNum)

		if cve.OpeneulerScore >= maxScore {
			maxScore = cve.OpeneulerScore
			if cve.Theme != "" && cve.AffectedProduct != "" {
				theme := strings.ReplaceAll(cve.Theme, "\n\n", "\r\n\r\n")
				theme = taskhandler.XmlSpecCharHand(theme)
				topic = strings.ReplaceAll(theme, cve.AffectedProduct, impl.joinVersion(sb))
			}
		}

		// Choose the highest security level in cves, as security level in bulletin
		for k, v := range dp.SequenceSeverityLevel {
			if v == cve.SeverityLevel && k > highestLevelIndex {
				highestLevelIndex = k
			}
		}
	}

	return DocumentNotes{
		Note: []Note{
			{
				Title:   "Synopsis",
				Type:    "General",
				Ordinal: "1",
				XmlLang: lang,
				Note:    fmt.Sprintf("%s security update", sb.Component),
			},
			{
				Title:   "Summary",
				Type:    "General",
				Ordinal: "2",
				XmlLang: lang,
				Note:    fmt.Sprintf("An update for %s is now available for %s", sb.Component, impl.joinVersion(sb)),
			},
			{
				Title:   "Description",
				Type:    "General",
				Ordinal: "3",
				XmlLang: lang,
				Note:    taskhandler.XmlSpecCharHand(strings.Trim(description, "\r\n\r\n")),
			},
			{
				Title:   "Topic",
				Type:    "General",
				Ordinal: "4",
				XmlLang: lang,
				Note:    topic,
			},
			{
				Title:   "Severity",
				Type:    "General",
				Ordinal: "5",
				XmlLang: lang,
				Note:    dp.SequenceSeverityLevel[highestLevelIndex],
			},
			{
				Title:   "Affected Component",
				Type:    "General",
				Ordinal: "6",
				XmlLang: lang,
				Note:    sb.Component,
			},
		},
	}
}

func (impl bulletinImpl) documentReferences(sb *domain.SecurityBulletin) DocumentReferences {
	selfUrl := []CveUrl{
		{
			Url: impl.cfg.SecurityBulletinUrlPrefix + sb.Identification,
		},
	}

	var cveUrl, other []CveUrl
	for _, cve := range sb.Cves {
		url := impl.cfg.SecurityCveUrlPrefix + fmt.Sprintf("cveId=%s", cve.CveNum)
		cveUrl = append(cveUrl, CveUrl{Url: url})

		otherUrl := "https://nvd.nist.gov/vuln/detail/" + cve.CveNum
		other = append(other, CveUrl{Url: otherUrl})
	}

	return DocumentReferences{
		CveReference: []CveReference{
			{
				Type:   "Self",
				CveUrl: selfUrl,
			},
			{
				Type:   "openEuler CVE",
				CveUrl: cveUrl,
			},
			{
				Type:   "Other",
				CveUrl: other,
			},
		},
	}
}

func (impl bulletinImpl) ProductTree(sb *domain.SecurityBulletin) *ProductTree {
	getCpe := func(v string) string {
		t := strings.Split(v, "-")
		return fmt.Sprintf("cpe:/a:%v:%v:%v", t[0], t[0], strings.Join(t[1:], "-"))
	}

	var productOfVersion []FullProductName
	for _, v := range sb.AffectedVersion {
		productOfVersion = append(productOfVersion, FullProductName{
			ProductId:       v,
			Cpe:             getCpe(v),
			FullProductName: v,
		})
	}

	branchOfVersion := OpenEulerBranch{
		Type:            productName,
		Name:            openeuler,
		FullProductName: productOfVersion,
	}

	branches := []OpenEulerBranch{
		branchOfVersion,
	}

	for arch, products := range sb.ProductTree {
		var productOfArch []FullProductName
		for _, p := range products {
			productOfArch = append(productOfArch, FullProductName{
				ProductId:       p.ID,
				Cpe:             getCpe(p.CPE),
				FullProductName: p.FullName,
				IsEpol:          p.IsEpol,
			})
		}

		branch := OpenEulerBranch{
			Type:            packageArch,
			Name:            arch,
			FullProductName: productOfArch,
		}

		branches = append(branches, branch)
	}

	return &ProductTree{
		Xmlns:           impl.cfg.XmlnsProd,
		OpenEulerBranch: branches,
	}
}

func (impl bulletinImpl) HotPatchTree(sb *domain.SecurityBulletin) *HotPatchTree {
	affectBranchListx := strings.Split(sb.AffectedVersion[0], "-")
	cpe := fmt.Sprintf("cpe:/a:%s:%s:%s",
		affectBranchListx[0], affectBranchListx[0], strings.Join(affectBranchListx[1:], "-"))

	branch := []OpenEulerBranch{{
		Type: productName,
		Name: openeuler,
		FullProductName: []FullProductName{
			{
				ProductId:       sb.AffectedVersion[0],
				Cpe:             cpe,
				FullProductName: sb.AffectedVersion[0],
			},
		},
	}}

	for _, v := range sb.PatchUrl {
		// v: http://121.36.84.172/hotpatch/openEuler-20.03-LTS/aarch64/Packages/patch-openssl-1.1.1m-15.oe2203sp1-HP1-1-1.aarch64.rpm
		split := strings.Split(v, "/")
		rpm := split[len(split)-1]

		// rmp: patch-openssl-1.1.1m-15.oe2203sp1-HP1-1-1.aarch64.rpm
		t := strings.Split(rpm, ".")
		// arch: aarch64
		arch := t[len(t)-2]
		// productId: patch-openssl-1.1.1m-15.oe2203sp1-HP1-1-1
		var productId string
		if len(t) > 3 {
			productId = strings.Join(t[:len(t)-2], ".")
		} else {
			productId = t[0]
		}

		if _, ok := archs[arch]; !ok {
			logs.Error("arch %s is invalid", arch)

			continue
		}

		b := OpenEulerBranch{
			Type: packageArch,
			Name: arch,
			FullProductName: []FullProductName{
				{
					ProductId:       productId,
					Cpe:             cpe,
					FullProductName: rpm,
				},
			},
		}

		branch = append(branch, b)
	}

	return &HotPatchTree{
		Xmlns:  impl.cfg.XmlnsProd,
		Branch: branch,
	}
}

func (impl bulletinImpl) vulnerability(sb *domain.SecurityBulletin) []Vulnerability {
	var vs []Vulnerability
	xmlns := impl.cfg.XmlnsCvrf
	if sb.IsColdPatch() {
		xmlns = "http://www.icasi.org/CVRF/schema/vuln/1.1"
	}

	for k, cve := range sb.Cves {
		var productIds []ProductId
		for _, v := range sb.AffectedVersion {
			productIds = append(productIds, ProductId{ProductId: v})
		}

		vul := Vulnerability{
			Ordinal: strconv.Itoa(k + 1),
			Xmlns:   xmlns,
			CveNotes: CveNotes{
				CveNote: CveNote{
					Title:   "Vulnerability Description",
					Type:    "General",
					Ordinal: "1",
					XmlLang: lang,
					Note:    taskhandler.XmlSpecCharHand(cve.CveBrief),
				},
			},
			ReleaseDate: sb.Date,
			CVE:         cve.CveNum,
			ProductStatuses: ProductStatuses{
				Status: Status{
					Type:      "Fixed",
					ProductId: productIds,
				},
			},
			Threats: Threats{
				Threat: Threat{
					Type:        "Impact",
					Description: cve.SeverityLevel,
				},
			},
			CVSSScoreSets: CVSSScoreSets{
				ScoreSet: ScoreSet{
					BaseScore: fmt.Sprintf("%.1f", cve.OpeneulerScore),
					Vector:    cve.OpeneulerVector,
				},
			},

			Remediations: Remediations{
				Remediation: Remediation{
					Type:        "Vendor Fix",
					Description: fmt.Sprintf("%s security update", sb.Component),
					Date:        sb.Date,
					Url:         impl.cfg.SecurityBulletinUrlPrefix + sb.Identification,
				},
			},
		}

		vs = append(vs, vul)
	}

	return vs
}
